﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="FormSizerUi.cs" company="Inframon Ltd">
//   Copyright (C) 2013 Inframon Ltd, All Rights Reserved.
// </copyright>
// <summary>
//   The form sizer ui.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace DPSizer
{
    using System;
    using System.ComponentModel;
    using System.IO;
    using System.Runtime.InteropServices;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Windows.Forms;

    /// <summary>
    /// The form sizer ui.
    /// </summary>
    public partial class FormSizerUi : Form
    {
        private long totalFiles = 0;

        public static IntPtr InvalidHandleValue = new IntPtr(-1);

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        public struct Win32FindDataw
        {
            public FileAttributes dwFileAttributes;

            internal System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;

            internal System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;

            internal System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;

            public int nFileSizeHigh;

            public int nFileSizeLow;

            public int dwReserved0;

            public int dwReserved1;

            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
            public string cFileName;

            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
            public string cAlternateFileName;
        }

        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern IntPtr FindFirstFileW(string lpFileName, out Win32FindDataw lpFindFileData);

        [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
        public static extern bool FindNextFile(IntPtr hFindFile, out Win32FindDataw lpFildFileData);

        [DllImport("kernel32.dll")]
        public static extern bool FindClose(IntPtr hFindFile);

        /// <summary>
        /// The file path.
        /// </summary>
        private string filePath;

        /// <summary>
        /// Initializes a new instance of the <see cref="FormSizerUi"/> class.
        /// </summary>
        public FormSizerUi()
        {
            this.InitializeComponent();
        }

        /// <summary>
        /// The update text box delegate.
        /// </summary>
        /// <param name="textbox">The textbox.</param>
        /// <param name="value">The value.</param>
        private delegate void UpdateTextBoxDelegate(TextBox textbox, string value);

        /// <summary>
        /// Returns the size of the specified path and recurses if required.
        /// </summary>
        /// <param name="sourceDir">The path to search.</param>
        /// <param name="recurse">Set to true if you want to recurse.</param>
        /// <returns>A <see cref="long"/> of the total size in bytes.</returns>
        private static long DirSize(string sourceDir, bool recurse)
        {
            try
            {
                long size = 0;
                var fileEntries = Directory.GetFiles(sourceDir);

                foreach (var fileName in fileEntries)
                {
                    Interlocked.Add(ref size, (new FileInfo(fileName)).Length);
                }

                if (recurse)
                {
                    var subdirEntries = Directory.GetDirectories(sourceDir);

                    Parallel.For<long>(
                        0,
                        subdirEntries.Length,
                        () => 0,
                        (i, loop, subtotal) =>
                            {
                                if ((File.GetAttributes(subdirEntries[i]) & FileAttributes.ReparsePoint)
                                    != FileAttributes.ReparsePoint)
                                {
                                    subtotal += DirSize(subdirEntries[i], true);
                                    return subtotal;
                                }

                                return 0;
                            },
                        (x) => Interlocked.Add(ref size, x));
                }

                return size;
            }
            catch (Exception exception)
            {
                MessageBox.Show(exception.Message, @"Exception", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return 0;
            }
        }

        /// <summary>
        /// Counts the number of files in a directory.
        /// </summary>
        /// <param name="sourceDir">The path to search.</param>
        /// <returns>A <see cref="long"/> with the number of files.</returns>
        private void CountFiles(string sourceDir)
        {
            if (File.Exists(sourceDir))
            {
                ++totalFiles;
            }
            else
            {
                var hFind = InvalidHandleValue;

                try
                {
                    Win32FindDataw find;
                    hFind = FindFirstFileW(sourceDir + @"\*", out find);
                    if (hFind != InvalidHandleValue)
                    {
                        do
                        {
                            if (find.cFileName == "." || find.cFileName == "..")
                            {
                                continue;
                            }

                            var fullPath = sourceDir + (sourceDir.EndsWith("\\") ? string.Empty : "\\") + find.cFileName;

                            if ((find.dwFileAttributes & FileAttributes.Directory) != 0)
                            {
                                this.CountFiles(fullPath);
                            }
                            else
                            {
                                ++totalFiles;
                            }
                        }
                        while (FindNextFile(hFind, out find));
                    }
                }
                finally
                {
                    if (hFind != InvalidHandleValue)
                    {
                        FindClose(hFind);
                    }
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ButtonCalculateClick(object sender, EventArgs e)
        {
            if (string.IsNullOrEmpty(this.textBoxPath.Text))
            {
                MessageBox.Show(
                    @"Please enter a valid local or UNC path in the box provided.",
                    @"Invalid Input",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);
                return;
            }

            this.filePath = this.textBoxPath.Text;
            this.backgroundWorker.RunWorkerAsync();
        }

        /// <summary>
        /// Updates a textbox value using a delegate for cross-thread support.
        /// </summary>
        /// <param name="textBox">The textbox control.</param>
        /// <param name="value">The value to set the control to.</param>
        private void UpdateTextBox(TextBox textBox, string value)
        {
            if (textBox.InvokeRequired)
            {
                textBox.Invoke(new UpdateTextBoxDelegate(this.UpdateTextBox), new object[] { textBox, value });
            }
            else
            {
                textBox.Text = value;
            }
        }

        /// <summary>
        /// The background worker for calculating the total size.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void BackgroundWorkerDoWork(object sender, DoWorkEventArgs e)
        {
            this.totalFiles = 0;
            this.UpdateTextBox(this.textBoxNumFiles, string.Empty);
            this.UpdateTextBox(this.textBoxFileSize, string.Empty);
            this.UpdateTextBox(this.textBoxDpSize, string.Empty);

            var fileSize = DirSize(this.filePath, true);
            var bloated = (fileSize * 10 / 100) + fileSize;
            this.CountFiles(this.filePath);

            this.UpdateTextBox(this.textBoxNumFiles, string.Format("{0:n0}", this.totalFiles));
            this.UpdateTextBox(this.textBoxFileSize, string.Format("{0:0.##}", this.FormatBytes(fileSize)));
            this.UpdateTextBox(this.textBoxDpSize, string.Format("{0:0.##}", this.FormatBytesRounded(bloated)));
        }

        /// <summary>
        /// Formats bytes into the correct value with a suffix.
        /// </summary>
        /// <param name="bytes">The <see cref="long"/> value in bytes.</param>
        /// <returns>A <see cref="string"/> with the formatted size.</returns>
        private string FormatBytes(long bytes)
        {
            string[] suffix = { "B", "KB", "MB", "GB", "TB" };

            var i = 0;
            double dblSByte = bytes;

            if (bytes > 1024)
            {
                for (i = 0; (bytes / 1024) > 0; i++, bytes /= 1024)
                {
                    dblSByte = bytes / 1024.0;
                }
            }

            return string.Format("{0:0.##} {1}", dblSByte, suffix[i]);
        }

        /// <summary>
        /// Formats bytes rounded into the correct value with a suffix.
        /// </summary>
        /// <param name="bytes">The <see cref="long"/> value in bytes.</param>
        /// <returns>A <see cref="string"/> with the formatted size.</returns>
        private string FormatBytesRounded(long bytes)
        {
            string[] suffix = { "B", "KB", "MB", "GB", "TB" };

            var i = 0;
            double dblSByte = bytes;

            if (bytes > 1024)
            {
                for (i = 0; (bytes / 1024) > 0; i++, bytes /= 1024)
                {
                    dblSByte = Math.Ceiling(bytes / 1024.0);
                }
            }

            return string.Format("{0:0.##} {1}", dblSByte, suffix[i]);
        }
    }
}
